﻿package seo_crawler;

use strict;
use warnings;
use URI;
use Data::Dumper;

use LWP::UserAgent;
use HTTP::Request::Common;
use HTML::TokeParser;

use Digest::MD5 qw(md5 md5_hex md5_base64);

use Encode;

$|=1;
#use lib './lib';
#use GraphViz;

# для возможности запуска из под шедулера
use FindBin;
use lib $FindBin::Bin;

use HTTP::Cookies;

sub new
{
	my $class=shift;
	my $url=shift;
	my $max_level=shift;
#	my $base_uri=shift;

	my $base_domain=shift;
	my $hndl_url=shift;
	my $rep_dir=shift;
	my $conf_rep=shift;

 	my $self={};

	$self->{kernel}={};

	$self->{level}=0;

 	$self->{url} = $url; # стартовый URL
 	$self->{max_level}=$max_level; # максимальный уровень прохода сайта 0 - только главная, 1 - главная и страницы в одном клике от главной
#	$self->{base_uri}=$base_uri;
	$self->{base_uri}=$url;
	$self->{base_domain}=$base_domain; # базовые домен например для trudogolik.com.ua - trudogolik.com.ua
	$self->{hndl_url}=$hndl_url;
	$self->{rep_dir}=$rep_dir;
	$self->{conf_rep}=$conf_rep;

	$self->{outer_links_count}=0; # к-ство внешних ссылок
	$self->{outer_links_count_nf_and_ni}=0; # количество внешних ссылок с rel=nofollow и в <noindex>
	$self->{outer_links_count_not_nf_and_ni}=0; # количество внешних ссылок без rel=nofollow и в <noindex>
	$self->{outer_links_count_not_ni}=0; # количество внешних ссылок только c rel=nofollow
	$self->{outer_links_count_not_nf}=0; # количество внешних ссылок только в <noindex>


#	$self->{ua}=LWP::UserAgent->new(agent => 'Lynx/2.8.7dev.9 libwww-FM/2.14');
	# $self->{ua}=LWP::UserAgent->new(agent => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)');
	$self->{ua}=LWP::UserAgent->new(agent => 'seo_crawler v 1.0.4b by http://wr-blog.ru/');
	$self->{ua}->cookie_jar( {} );


#	$self->{graph} = GraphViz->new(concentrate=>1,rankdir=>1);

#	print Dumper($self);

#	push @{ $self->{ua}->requests_redirectable }, 'GET';
#	push @{ $self->{ua}->requests_redirectable }, 'POST';

	bless ($self,$class);

	return $self;
}


sub go
{
	my $self=shift;

	# В начале проходим главную страницу и определяем количество внутренних страниц на ней
	
	$self->walk();

	# пока есть непройденные ссылки
	while ($self->{max_level}>=$self->{level})
	{
		my $link_md5_key=$self->get_not_walked_link($self->{level});

#		print $link_md5_key;
		# пройдены все ссылки этого уровня вложенности
		if (!defined($link_md5_key))
		{
			$self->{level}++;
			next;
		}
#		теперь "стартовой" является уже другая страница
		$self->{url}=$self->{kernel}->{$self->{level}}->{$link_md5_key}->{url};

#		print $self->{url}; print "\n";

#		print Dumper($self->{kernel});

		$self->walk();
	}

#	my $fl = $ARGV[0] || 'result.svg';

#	open my $z, '>', $fl;
#	print {$z} $self->{graph}->as_svg; # или $g->as_png; но оно Вам надо?))
#	close $z;

}


sub walk
{
	my $self=shift;

	# если главная страница
#	if ($self->{max_level}==1)
#	{

#	}

# Нужно учитывать то, что если с главной идет редирект то теперь  	$self->{base_uri} - должен менятся
# есть движки у которых редирект реализован или javascript или с помощью тега meta
# <meta http-equiv="Refresh" content="0; URL=Neitrino/index.php">
# или и тем и другим

#    http_accept       => [qw( text/html text/plain text/xhtml )],

#  $self->{graph}->add_node($self->{url},rank=>$self->{level},URL=>$self->{url});
	
	# получаем контент страницы
	my $c = get_content($self->{ua},$self->{url},$self->{level});

	# результирующая структура
	my $result_strct;

	if (defined($c))
	{

		#	2 CHECK => http://lib.ru/java_vs_ax.txt_Contents    [text/html; charset=windows-1251 => 200 OK ]   GET => 200 OK
		#Use of uninitialized value in subroutine entry at seo_crawler.pl line 130.
	
	# получаем ссылки с HTML страницы, контент которой хранится в скаляре $c
	$result_strct = get_links($c,$self->{base_uri},$self->{base_domain},$self->{level},$self->{url},$self);

#	print Dumper($result_strct);

	# $self->{hndl_url} содержит указатель на функция которая будет вызвана и которой будут переданы необходимые параметры.
	# HNDL вызываем обработчик
	$self->{hndl_url}($result_strct,$self->{rep_dir},$self->{conf_rep},$self);


	# сохраняем результирующу структуру в хэше с ключом, который одновременно является и уровнем данной страницы

	$self->{kernel}->{$self->{level}}->{md5_enc_url($self->{url})}=$result_strct;

#	my $old_level=$self->{level};

	# если мы достигли неободимого уровня то следующие ссылки не обрабатываем, иначе
	# проходим все внутренние ссылки пройденной нами страницы и проверям их md5 на то пройдены ли они
	# если не пройденў 
	#

	# сохряняем ссылку на структуру, для того чтобы позже извлечь внутренние ссылки
	my $tmp_inner_links=$self->{kernel}->{$self->{level}}->{md5_enc_url($self->{url})}->{inner_links}->{all_links};


	# отмечаем, что данная ссылка уже посещалась роботом
	$self->{kernel}->{walked_links}->{md5_enc_url($self->{url})}=1;

	# добавляем новые ссылки на соответствующий уровень. То есть конечная структура имеет следующий вид
	#$VAR1 = {
	#          '1' => {
	#                   '7ebe9ad9f6be7273b3ae01bfe3d533bd' => {
	#                                                           'url' => 'http://www.site/ua/'

	# проходим в цикле все внутренние ссылки которые были получены на странице и проверяем есть ли уже посещенные
	# Если есть какая-либо ссылка, которая еще не посещалась до добавляем ее в структуру в соответствующий уровень.

	my $tmp_old_level=$self->{level};

#	$self->{level}++;

	my $tmp_next_level=$self->{level}+1;

	foreach my $el (@{$tmp_inner_links})
	{
#$VAR1 = {
#          'orig_url' => 'http://www.site/ua/',
#         'can_url' => 'http://www.site/ua/',
#         'md5' => '39257f07f72cf6ab338f78fb3dc928ff'
#        };

		if (!(exists($self->{kernel}->{walked_links}->{md5_enc_url($el->{can_url})})))
		{

#			$self->{graph}->add_edge($self->{url}=>$el->{can_url});
			
			$self->{kernel}->{$tmp_next_level}->{md5_enc_url($el->{can_url})}->{url}=$el->{can_url};
#			print $el->{can_url}; print "\n";						
		}
#		print Dumper($el);

#		exit(0);
	}




	$self->{kernel}->{walked}->{md5_enc_url($self->{url})}=1;


	}
	else
	{
		# значит ссылка указывает на файл тип которого не txt/html или txt/xhtml
		# Просто отмечаем эту ссылку как пройденную
		#
		$self->{kernel}->{walked}->{md5_enc_url($self->{url})}=1;
	}

	return 1; # в случае если пройдены все ссылки возращаем 1, что означает успешное завершение работы

}

sub get_not_walked_link
{
	# возращаемое значение: md5 внутренней ссылки которая будет пройдена следующей
	# 
	#
	

	# на входе уровень ссылки которую необходимо получить, например 2 (то есть в 2-ух кликах от главной). Если ссылок
	# этого уровня больше нет то возращает undef, иначе ссылке. При поиске ссылки учитывается, чтобы она уже не посещалась
	# роботом.

	my $self=shift;
	my $level=shift; 

	# берем ссылки предыдущего уровня и проверяем по каждой из них есть ли еще не пройденный ссылки
#	my $tmp_l=$level-1;

#	print Dumper ($self->{kernel}->{$self->{level}});


	# проходим все ссылки заданного уровня
	my @links=keys (%{$self->{kernel}->{$level}});

	foreach my $link_md5_key (@links)
	{
#		если ссылка не отмечена как пройденная возращаем ее, она должна быть пройдена
		if (!exists ($self->{kernel}->{walked}->{$link_md5_key}))
		{
#			print $link_md5_key;
			return $link_md5_key;

		}
	}


	# если больше нет ссылок этого уровня
	return undef;

}


sub get_content
{
  my $ua=shift;
	my $url=shift;
	my $level=shift;

#	my $head_req=$self->{ua}->request(HEAD
#   my ( $content_type, $document_length, $modified_time ) =
#               $self->{ua}->head( $self->{url} );

#	print Dumper($self->{ua}->head( $self->{url} ));
#	print ($content_type);

	print $level." ";
	print "CHECK => ".$url;


	my $res;

	my $head_req = HTTP::Request->new(HEAD => $url);


	my $http_status;

	# сначала отправляем HTTP HEAD запрос для того чтобы понять нужен ли нам этот документ. В этом аспекте нас интересуют HTML документы
	for (my $n=1; $n<=3; $n++)
	{
		$res=${ua}->request($head_req);

		$http_status=$res->status_line;

		if ($res->is_error) 
		{   
#			print "[ ".$url." ]\n";
#			print "Warning: ", $res->status_line, "\n";  


			sleep($n*1);
			$head_req = HTTP::Request->new(HEAD => $url);
		}
		else
		{
#			if ($res->is_redirect)
#			{
#				print Dumper($res);
#			}

			last;
		}
	}

	print "    [".$res->header('content-type')." => ".$http_status." ]";
#	print Dumper($res);

	# документа HTML или XHTML ? 
	# Если нет возращаем undef что проверяется функцией, которая получает ссылки с HTML страницы, тоесть данный документ не будет обработана
	if (!(($res->header('content-type') =~/text\/html/) || ($res->header('content-type') =~ /text\/xhtml/)))
	{                               
		print "\n";
		return undef;
	}

	# теперь отправляем HTTP GET запрос
	my $req = HTTP::Request->new(GET => $url);

#	print Dumper($req);

	for (my $n=1; $n<=3; $n++)
	{
		$res=${ua}->request($req);

		$http_status=$res->status_line;

		if ($res->is_error) 
		{   
#			print "[ ".$url." ]\n";
#			print "Warning: ", $res->status_line, "\n";  

			sleep($n*1);
			$req=HTTP::Request->new(GET=>$url);
		}
		else
		{
#			if ($res->is_redirect)
#			{
#				print Dumper($res);
#			}

			last;
		}
	}

	print "   GET => ".$http_status."\n";

	my $content=$res->content;
	
	# возращаем ссылку на контент
	return \$content;
}



sub get_links
{
	# задача метода - отпарсить внутренние и внешние ссылки с веб страницы, сохранить их в структуре и вернуть вызывающему
	# коду

	my $content=shift;
	my $base_uri=shift;
	my $base_domain=shift;
	my $level=shift;
	my $url=shift;
	my $self=shift;
	
	# возращаемый результат
	my $result={};

	$self->{pages_count}++;

	# получам базовый домен, на основании которого будут вычислятся внутренние ссылки с URL
	#
	if (!defined($base_domain))
	{
		$base_domain = get_domain_from_url($url);
	}	

	my @inner_links;
	my @outer_links;

	my $unique_inner_links={};

	my $p = HTML::TokeParser->new($content);

	my @noindex;

	while (my $token = $p->get_token()) 
	{
		# учитываем теги noindex используя "стек" реализованный с помощью массива
		# таким образом, если вы встречаем ссылку а в массиве есть элементы - значит ссылка в noindex
		# с другой стороны, тег noindex может быть например не закрытым, но как будет работать поисковик?
		# Кто его знает...

 		if ($token->[0] eq 'S' && $token->[1] eq 'noindex')
		{
			push @noindex,1; 
		}
		elsif ($token->[0] eq 'E' && $token->[1] eq 'noindex')
		{
			shift @noindex; 
		}
		elsif ($token->[0] eq 'S' && $token->[1] eq 'a' && defined ($token->[2]->{href}))
		{
			#	проверям чтобы это не была ссылка на якорь типа #...
			#  здесь учитываем то, что в joomla используются ссылки например такого вида index.php?option=com_weblinks&catid=3&Itemid=4#
			if ($token->[2]->{href}!~/#/)
#			if (1)
			{
				my $can_url=get_canonical_url($token->[2]->{href},$base_uri);

				# проверяем внутрення ли это ссылка или нет
				# здесь необходимо учитывать, что $base_domain можно получить с помощью get_domain_from_url
				# а можно указывать явно. Это сделано для того, чтобы можно было, например, считать ссылки на домены 3-ьего
				# внутренними
				# С другой стороны ссылки на поддомены - это не внешние ссылки поэтому здесь необходимо добавить
				# механизм, учитывающий это.
				

				# 
				if ($can_url=~/(http|https)\:\/\/[^\/]*$base_domain/)
				{
					push @inner_links, {can_url=>$can_url,orig_url=>$token->[2]->{href},md5=>md5_enc_url($can_url)};
					$unique_inner_links->{md5_enc_url($can_url)}=$#inner_links;
				}
				# иначе ссылка внешняя
				elsif ($can_url=~/(http|https)/)
				{
				  my $nofollow=0;
				  my $noindex=0;


					# если ссылка в nofollow
 					if (defined($token->[2]->{rel}) && $token->[2]->{rel} =~ /nofollow/i)
					{
						$nofollow=1;						
					}

 					if (!$nofollow && defined($token->[2]->{class}) && $token->[2]->{class} =~ /postlink/i)
					{
						print "POSTLINK = > ". $token->[2]->{href} . "\n";
					}


					if ($#noindex+1>0)
					{
						$noindex=1;
					}

					# учитываем ссылки без nofollow и без noindex а также каждую по отдельности
					if ($noindex && $nofollow)
					{
						$self->{outer_links_count_nf_and_ni}++;
					}
					elsif (!$noindex && !$nofollow)
					{
						$self->{outer_links_count_not_nf_and_ni}++;
					}
					elsif (!$noindex && $nofollow)
					{
						$self->{outer_links_count_not_ni}++;
					}
					elsif ($noindex && !$nofollow)
					{
						$self->{outer_links_count_not_nf}++;
					}



					# для внешней ссылки похоже нет смысла в канонизации, но я пока это оставлю
					push @outer_links, {can_url=>$can_url,
												orig_url=>$token->[2]->{href},
												noindex=>$noindex,
												nofollow=>$nofollow,
												md5=>md5_enc_url($can_url)
											};

					$self->{outer_links_count}++;
				}

			}
		}
	} # while

	# выбираем все уникальные ссылки сохраная номер индекса в массиве
	my @unique_inner_links;

	foreach my $key (keys (%{$unique_inner_links}))
	{
		push @unique_inner_links,$unique_inner_links->{$key};
	}


	$result->{url}=$url;
#	$result->{md5_enc_url($url)}->{inner_links}->{all_links}=\@inner_links;
#	$result->{md5_enc_url($url)}->{outer_links}->{all_links}=\@outer_links;

#	$result->{md5_enc_url($url)}->{inner_links}->{uniq}=\@unique_inner_links;

	$result->{inner_links}->{all_links}=\@inner_links;
	$result->{outer_links}->{all_links}=\@outer_links;

	$result->{inner_links}->{uniq}=\@unique_inner_links;


#	$result->{$url}->{level}=$level;
	
#	$result->{$url}->{walked}=1;


	return $result;
}

sub md5_enc_url
{
	# Эта функция только для ссылок
	# на входе строка, на выходе md5 строки в 16-ричном виде
	my $str=shift;

	# мы не можем расчитывать на то, как в конечном счете будет реализован модуль URL, поэтому удаляем последний слэш,
	# если он есть, а также пробельные символы в конце
	$str=~s/(.+?)\s+$/$1/;
	$str=~s/(.+?)\/+$/$1/;

	my $ctx = Digest::MD5->new;

	
	$ctx->add($str);

	return $ctx->hexdigest;

}

sub get_domain_from_url
{
	# получаем домен из URL, поддерживаются только URL вида http|https
	my $url=shift;

	my (undef,$domain)=$url=~/(http|https)\:\/\/([^\/]+)/;

	return $domain;
}

sub get_canonical_url
{
	# приведение URL к канонизированному виду. Для этого в качестве второго параметра необходимо передать URL сайта
	# канонизированного вида
	my $str=shift;
	my $base_uri=shift;

	my $uri = URI->new_abs($str, $base_uri);

	return $uri->as_string;
}


return 1;

